#!/usr/bin/python
#-----------------------------------------------------------------------------|
"""
Parser module: Parse input files/dirs

Part of Expenses.
License GPL.

Syntax parsed for QFX files (i.e. OFX for Quicken):
- an non-XML header
- SGML content after <OFX>
- Not quite XML so most non-structural "value" tags are not closed properly.
Relevant tree:

//OFX/BANKMSGSRSV1/STMTTRNRS* => one per account
    /TRNUID/STMTRS/CURRDEF/BANKACCTFROM/BANKID => routing number
    /TRNUID/STMTRS/CURRDEF/BANKACCTFROM/ACCTID => account it
    /TRNUID/STMTRS/CURRDEF/BANKTRANLIST/
        /DTSTART => yyyymmddhhmmss.xxx[z]
            /DTEND  => yyyymmddhhmmss.xxx[z]
    /TRNUID/STMTRS/CURRDEF/BANKTRANLIST/DTSTART/DTEND/STMTTRN* => statement transaction
        <TRNTYPE> DEBIT
        <DTPOSTED> yyyymmddhhmmss
        <TRNAMT> -value or +value
        <FITID> num-id (ignore)
        <REFNUM> ignore
        <NAME> ...
        <MEMO> ...

We can get away by not parsing XML using a DOM/SAX; just look for the elements
keywords then grab the value till the next tag and trim whitespace.

"""
__author__ = "ralfoide@gmail.com"

import os

import glob

#------------------------
class Transaction(object):
    def __init__(self, date, value, name, memo, acc):
        self.date = date
        self.value = value
        self.name = name.replace("&amp;", "&")
        self.memo = memo
        self.acc = acc
    
    def __str__(self):
        return self.str(None)
    
    def str(self, account_names=None):
        d = self.__dict__
        acc = d["acc"]
        if account_names and acc in account_names:
            d = dict(self.__dict__)
            d["acc"] = account_names.get(acc)
        return "[%(date)s] %(value)9.2f => '%(name)32s' ('%(memo)42s') [%(acc)s]" % d

#------------------------
class QfxReader(object):
    def __init__(self, file_path):
        text = ""
        f = None
        try:
            f = file(file_path, "r")
            need_space = False
            while True:
                line = f.readline()
                if not line: break
                line = line.strip()
                if need_space and not line.startswith("<"):
                    text += " "
                text += line
                need_space = not line.endswith(">")
        finally:
            if f: f.close()
        self._text = text
        self._pos = 0

    def ReadTag(self, tag, exclude=None):
        """
        Finds the requested tag and returns it's node text value.
        Advances the current pos to the end of the next tag.
        """
        text = self._text
        pos = text.find(tag, self._pos)
        if exclude:
            exc = text.find(exclude, self._pos)
        else:
            exc = -1
        if pos >= 0 and (exc == -1 or pos < exc):
            start = pos + len(tag)
            pos = text.find("<", start)
            if pos >= 0:
                self._pos = pos
                end = pos - 1
            else:
                end = len(text)
            found = text[start:end]
            return found
        return ""

    def HasTag(self, tag, exclude=None):
        """
        Finds the requested tag and returns true if found, false otherwise.
        Parsing stops before the exclude tag if found.
        Advances the current pos to the end of the tag.
        """
        text = self._text
        pos = text.find(tag, self._pos)
        if exclude:
            exc = text.find(exclude, self._pos)
        else:
            exc = -1
        if pos >= 0 and (exc == -1 or pos < exc):
            start = pos + len(tag)
            self._pos = start
            return True
        return False
        

#------------------------
class Parser(object):
    """
    Parse input files/dirs
    """
    def __init__(self, log):
        self._log = log
        self._accounts = {}
        self._num_trans = 0

    def Accounts(self):
        return self._accounts

    def ParseDir(self, dir_path):
        self._log.Debug("parsing dir: %s", dir_path)
        for file in glob.glob1(dir_path, "*.?FX"):
            self.ParseFile(os.path.join(dir_path, file))
        self._log.Debug("Total  # trans: %d", self._num_trans)

    def ParseFile(self, file_path):
        self._log.Debug("parsing file: %s", file_path)

        q = QfxReader(file_path)
        if q.HasTag("<OFX>"):
            self._ReadAccounts(q)
    
    def _ReadAccounts(self, q):
        while q.HasTag("<STMTTRNRS>"):
            acc_id = q.ReadTag("<ACCTID>")
            if q.HasTag("<BANKTRANLIST>", "<STMTTRNRS>"):
                self._log.Debug("Acc ID: %s", acc_id)
                a = self._accounts.get(acc_id, [])
                if not acc_id in self._accounts:
                    self._accounts[acc_id] = a
                a.extend(self._ReadTransactions(q, acc_id))
    
    def _ReadTransactions(self, q, acc):
        s = []
        while q.HasTag("<STMTTRN>", "<STMTTRNRS>"):
            t = Transaction(q.ReadTag("<DTPOSTED>"),
                            self._ToFloat(q.ReadTag("<TRNAMT>")),
                            q.ReadTag("<NAME>"),
                            q.ReadTag("<MEMO>", "<STMTTRN>"),
                            acc)
            s.append(t)
            self._num_trans += 1
        self._log.Debug("  # trans: %s", len(s))
        return s
    
    def _ToFloat(self, s):
        try:
            return float(s)
        except ValueError:
            return 0


#------------------------
# Local Variables:
# mode: python
# tab-width: 4
# py-continuation-offset: 4
# py-indent-offset: 4
# sentence-end-double-space: nil
# fill-column: 79
# End:
